#
# This file is a part of the open-eBackup project.
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at
# http://mozilla.org/MPL/2.0/.
#
# Copyright (c) [2024] Huawei Technologies Co.,Ltd.
#
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
#

import locale
import os
import platform
import time

from common.common import read_result_file, touch_file
from common.file_common import delete_file, create_dir_recursive
from common.parse_parafile import get_env_variable
from oracle import logger
from oracle.common.backup_common import build_backup_database_sql, get_last_log_scn_info, get_online_redo_log_info, \
    read_additional_db_file, change_archive_log_unavailable, \
    exist_addition_file, cross_check_backup_dbf, cross_check_archive_log, get_all_table_space_from_pdb, \
    backup_orapw_file, get_pdb_data_files, get_all_pdbs
from oracle.common.common import get_database_info, get_datafile_mini_scn, create_pfile_from_spfile_linux, \
    chown_file_path_owner, list_pdbs
from oracle.common.constants import ScriptExitCode, PluginPathConstant, ArchiveLogMode, RETURN_INVALID, LOG_IS_VALID, \
    TaskType, ChannelsNum
from oracle.common.linux_common import get_linux_oracle_version, check_oracle_install_type_linux, \
    check_sqlplus_status_linux, check_rman_status_linux, execute_linux_rman_cmd
from oracle.common.windows_common import redirect_backup_path, create_dir, write_tmp_file, get_oracle_version, \
    check_oracle_install_type, check_sqlplus_status, check_rman_status, create_pfile_from_spfile, \
    execute_windows_rman_cmd, add_unix_timestamp, get_dir_file_info, create_empty_file


class OraclePdbBackupService(object):
    def __init__(self, pid, job_id, params):
        self._job_id = job_id
        self._pid = pid
        self.params = params
        self.ora_version = ""
        self.ora_pre_version = ""
        self.ora_pre_version_extend = ""
        self.db_is_cluster = 0
        self.db_user = self.params.get('UserName')
        self.db_password = self.params.get('Password')
        self.archive = self.params.get('LogPath')
        self.db_uuid = self.params.get('DBUUID')
        self.db_name = self.params.get('ParentName')
        self.last_backup_scn = self.params.get('LastBackupScn')
        self.last_backup_reset_logs_id = self.params.get('LastBackupResetLogsId')
        self.ip_port_info = self.params.get('IpPortInfo').split(";")
        self.channels = int(self.params.get('Channel'))
        self.qos = self.params.get('Qos')
        self.node_number = self.params.get('NodeNumber')
        self.truncate_log = self.params.get('truncateLog')
        self.backup_path_list = self.params.get('DataPath')
        self.asm_id_name = self.params.get('ASMInstanceName')
        self.level = self.params.get('Level')
        self.oracle_home = self.params.get('OracleHome')
        self.oracle_base = self.params.get('OracleBase')
        self.asm_user = self.params.get('ASMUserName')
        self.oracle_install_user = self.params.get('OracleInstallUser')
        self.oracle_group = self.params.get('OracleGroup')
        self.resetlogs_id = ''
        self.open_mode = ''
        self.from_scn = ''
        self.is_enc_bk = 0
        self.log_num = 0
        self.dbf_info = ''
        self.additional = ''
        self.backup_tmp = ''
        self.rman_enc_section = ''
        self.log_is_backed_up = ''
        self.main_backup_path = ''
        self.db_instance = self.params.get('InstanceName')
        self.result_file = ''
        self.result_content = ''
        self.login_db_params = {}
        self.run_rman_params = {}
        self.login_params_with_key = {}
        self.is_windows_os = True if platform.system().lower() == "windows" else False
        self.tmp_path = PluginPathConstant.WINDOWS_TMP_PATH if self.is_windows_os else PluginPathConstant.STMP_PATH
        self.pdb_names = self.params.get('PdbNames')
        self.pdb_names.append('PDB$SEED')
        logger.info(f"pdb_names={self.pdb_names}.")

    def init_login_params(self):
        self.login_db_params = {
            'pid': self._job_id,
            'instance_name': self.db_instance,
            'db_install_user': self.oracle_install_user,
            'db_user': self.db_user,
            'db_password': self.db_password,
            'oracle_home': self.oracle_home
        }

        self.run_rman_params = {
            'is_enc_bk': self.is_enc_bk,
            'instance_name': self.db_instance,
            'db_install_user': self.oracle_install_user,
            'db_user': self.db_user,
            'db_password': self.db_password,
            'rman_enc_section': self.rman_enc_section
        }

        self.login_params_with_key = {
            'pid': self._pid,
            'instance_name': self.db_instance,
            'db_install_user': self.oracle_install_user,
            'auth_pwd': f"job_protectObject_auth_authPwd_{self._pid}",
            'auth_key': f"job_protectObject_auth_authKey_{self._pid}"
        }

    def get_build_data_backup_params(self):
        return {
            'pid': self._job_id,
            'datafile_info': self.dbf_info,
            'backup_path_list': self.backup_path_list,
            'db_name': self.db_name,
            'level': self.level,
            'additional': self.additional,
            'ora_pre_version': self.ora_pre_version,
            'is_enc_bk': self.is_enc_bk,
            'qos': self.qos,
            'channels': self.channels,
            'db_is_cluster': self.db_is_cluster,
            'node_number': self.node_number,
            'ip_port_info': self.ip_port_info,
            'main_backup_path': self.main_backup_path,
            'backup_tmp': self.backup_tmp,
            'open_mode': self.open_mode,
            'oracle_home': self.oracle_home,
            'asm_instance': self.asm_id_name,
            'oracle_group': self.oracle_group,
            'oracle_user': self.oracle_install_user,
            'pdb_names': self.pdb_names,
        }

    def backup_data_pdb(self):
        ret_code = self.prepare_backup()
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Execute prepare backup failed, pid:{self._job_id}.")
            return ret_code

        enc_algo = self.params.get('EncAlgo')
        enc_key = self.params.get('EncKey')

        if enc_algo and enc_key:
            self.is_enc_bk = 1
            self.rman_enc_section = f"configure encryption for database on;\n" \
                                    f"configure encryption algorithm '{enc_algo}';\n" \
                                    f"set encryption on identified by \"{enc_key}\" only;\n" \
                                    f"set decryption identified by \"{enc_key}\";\n"
            logger.info(f"Backup database will enc, pid:{self._job_id}.")
        # 初始化登录rman/oracle常用参数
        self.init_login_params()
        ret_code = self.check_host_environment()
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Check host environment failed, pid:{self._job_id}.")
            return ret_code
        logger.info(f"Check cluster [db_is_cluster={self.db_is_cluster}], db_instance={self.db_instance}.")

        logger.info(f"PID={self._job_id};DBUUID={self.db_uuid};DBINSTANCE={self.db_instance};DBNAME={self.db_name};"
                    f"DBUSER={self.db_user};ASMInst={self.asm_id_name};ASMUSER={self.asm_user};"
                    f"IN_ORACLE_HOME={self.oracle_home};BACKUP_PATH_LIST={self.backup_path_list};"
                    f"ARCHIVE={self.archive};LEVEL={self.level};CHANNELS={self.channels};QOS={self.qos};"
                    f"ENCALGO={enc_algo};IPPORTINFO={self.ip_port_info};NODENUMBER={self.node_number}.")

        ret_code = self.prepare_data_backup()
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Prepare data backup failed, pid:{self._job_id}.")
            return ret_code
        ret_code = self.backup_data_file()
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Backup data file failed, pid:{self._job_id}.")
            return ret_code
        ret_code = self.backup_control_file()
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Backup control file failed, pid:{self._job_id}.")
            return ret_code
        ret_code = self.backup_additional_file()
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Backup additional file failed, pid:{self._job_id}.")
            return ret_code

        logger.info(f"Do backup success, pid: {self._job_id}.")
        return ScriptExitCode.SUCCESS

    def prepare_backup(self):
        self.backup_path_list = redirect_backup_path(self.backup_path_list)
        self.main_backup_path = self.backup_path_list.split(";")[0]
        if self.is_windows_os:
            self.additional = f"{self.main_backup_path}\\additional"
            self.backup_tmp = f"{self.main_backup_path}\\tmp"
        else:
            self.additional = f"{self.main_backup_path}/additional"
            self.backup_tmp = f"{self.main_backup_path}/tmp"

        logger.info(f"Get backup_path_list: {self.backup_path_list}, main_backup_path: {self.main_backup_path}, "
                    f"pid: {self._job_id}.")
        # 检查是否存在main_backup_path
        cnt = 1
        while cnt < 5:
            if not os.path.exists(self.main_backup_path):
                logger.error(f"Data path is invalid, pid:{self._job_id}.")
                time.sleep(3)
            else:
                break
            cnt += 1
        if not os.path.exists(self.main_backup_path):
            logger.error(f"Data path is invalid, pid:{self._job_id}.")
            return ScriptExitCode.ERROR_PARAM_INVALID
        if self.is_windows_os:
            self.log_is_backed_up = f"{LOG_IS_VALID} and name like '{self.main_backup_path}\\log\\arch_%'".upper()
        else:
            self.log_is_backed_up = f"{LOG_IS_VALID} and name like '{self.main_backup_path}/log/arch_%'"

        if self.channels == ChannelsNum.ZERO_CHANNELS_NUM:
            self.channels = ChannelsNum.DEFAULT_CHANNELS_NUM
            logger.warning(f"Setting channels number to {self.channels} by default.")
        if self.channels > ChannelsNum.MAX_CHANNELS_NUM:
            logger.error("Channel params is invalid.")
            return RETURN_INVALID
        return ScriptExitCode.SUCCESS

    def check_host_environment(self):
        logger.info(f"Enter function Check host environment, pid:{self._job_id}.")
        if self.is_windows_os:
            self.db_is_cluster = check_oracle_install_type()
            [self.ora_version, self.ora_pre_version, self.ora_pre_version_extend] = get_oracle_version(self._pid)
            if not check_sqlplus_status() or not check_rman_status():
                logger.error(f"check_sqlplus_status, check_rman_status failed.")
                return RETURN_INVALID
        else:
            self.db_is_cluster = check_oracle_install_type_linux()
            [self.ora_version, self.ora_pre_version, self.ora_pre_version_extend] = get_linux_oracle_version(
                self.oracle_install_user)
            if not check_sqlplus_status_linux(self.oracle_install_user) or not check_rman_status_linux(
                    self.oracle_install_user):
                logger.error(f"check_sqlplus_status, check_rman_status failed.")
                return RETURN_INVALID
        logger.info(f"Check cluster [db_is_cluster={self.db_is_cluster}], db_instance={self.db_instance}.")
        return ScriptExitCode.SUCCESS

    def prepare_data_backup(self):
        logger.info(f"Enter function prepare data backup, pid:{self._job_id}.")
        if self.is_windows_os:
            create_dir(self._job_id, self.backup_path_list, self.asm_id_name, self.oracle_home)
            create_dir(self._job_id, self.backup_tmp, self.asm_id_name, self.oracle_home)
            create_dir(self._job_id, self.archive, self.asm_id_name, self.oracle_home)
            create_dir(self._job_id, self.additional, self.asm_id_name, self.oracle_home)
            create_dir(self._job_id, os.path.join(self.additional, 'dbs'), self.asm_id_name, self.oracle_home)
            create_dir(self._job_id, os.path.join(self.main_backup_path, 'log'), self.asm_id_name, self.oracle_home)
        else:
            create_dir_recursive(self.backup_tmp)
            create_dir_recursive(self.additional)
            create_dir_recursive(os.path.join(self.additional, 'dbs'))
            create_dir_recursive(os.path.join(self.main_backup_path, 'log'))
            chown_file_path_owner(self.backup_tmp, self.oracle_install_user)
            chown_file_path_owner(self.additional, self.oracle_install_user)
            chown_file_path_owner(os.path.join(self.additional, 'dbs'), self.oracle_install_user)
            chown_file_path_owner(os.path.join(self.main_backup_path, 'log'), self.oracle_install_user)
        base_info = get_database_info(self.login_params_with_key, self._job_id)
        if not base_info:
            logger.error(f"Get database base info failed, pid:{self._job_id}")
            return RETURN_INVALID

        [log_mode, db_id, uniq_name, self.open_mode, incarnation_number, self.resetlogs_id] = base_info
        if log_mode != ArchiveLogMode.ARCHIVELOG.value:
            logger.error(f"Archive Mode=No Archive Mode, check archive mode failed, pid: {self._job_id}.")
            return ScriptExitCode.ERROR_ORACLE_NOARCHIVE_MODE

        logger.info(f"Write backup metadata info, pid: {self._job_id}.")
        db_info = f"{db_id};{uniq_name};{self.db_instance};{self.asm_user};{self.asm_id_name};{self.db_is_cluster};" \
                  f"{self.ora_pre_version_extend};{incarnation_number};{self.resetlogs_id};{self.ora_version}"
        write_tmp_file(os.path.join(self.additional, 'dbinfo'), db_info)

        oracle_info = f"ORACLE_BASE={self.oracle_base}\nORACLE_HOME={self.oracle_home}"
        write_tmp_file(os.path.join(self.additional, 'env_file'), oracle_info)

        return ScriptExitCode.SUCCESS

    def backup_data_file(self):
        data_min_scn = get_datafile_mini_scn(self._pid, self._job_id, self.db_instance, self.oracle_install_user)
        if not data_min_scn:
            logger.error(f"Min data file scn is invalid, pid:{self._job_id}.")
            return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED

        self.dbf_info = f"{self.tmp_path}/get_datafile_info_rst_{self._job_id}.sql"
        ret_code = get_pdb_data_files(self._pid, self.login_db_params, self.pdb_names, self.dbf_info)
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Get database data file info failed, pid:{self._job_id}.")
            delete_file(self.dbf_info)
            return ret_code

        ret_code = self.create_pfile_from_spfile()
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Create pfile failed, pid:{self._job_id}")
            return ret_code
        pdb_names = self.get_build_data_backup_params().get('pdb_names')
        # 校验备份信息
        cross_check_backup_dbf(self._job_id, self.run_rman_params, 0, self.db_name, pdb_names)
        cross_check_archive_log(self._job_id, self.run_rman_params, 0)

        backup_database_sql = f"{self.tmp_path}/backup_database_sql_{self._job_id}.sql"
        backup_database_rst = f"{self.tmp_path}/backup_database_rst_{self._job_id}.sql"
        data_backup_params = self.get_build_data_backup_params()
        build_backup_database_sql(backup_database_sql, data_min_scn, data_backup_params)
        start_time = time.strftime('%Y-%m-%d_%H:%M:%S', time.localtime(time.time()))
        logger.info("Start running RMAN to backup")
        if self.is_windows_os:
            ret_code = execute_windows_rman_cmd(self._job_id, self.run_rman_params, 1, backup_database_sql,
                                                backup_database_rst)
        else:
            chown_file_path_owner(backup_database_sql, self.oracle_install_user)
            touch_file(backup_database_rst)
            chown_file_path_owner(backup_database_rst, self.oracle_install_user)
            chown_file_path_owner(backup_database_sql, self.oracle_install_user)
            ret_code = execute_linux_rman_cmd(self._job_id, self.run_rman_params, 1, backup_database_sql,
                                              backup_database_rst)

        if ret_code != ScriptExitCode.SUCCESS:
            encoding = locale.getdefaultlocale()[1]
            logger.error(f"Backup database-{self.db_instance} failed, error_code:{ret_code}, pid:{self._job_id}, "
                         f"error is:{read_result_file(backup_database_rst, encoding=encoding)}")
            delete_file(backup_database_sql)
            delete_file(backup_database_rst)
            delete_file(self.dbf_info)
            return ret_code

        end_time = time.strftime('%Y-%m-%d_%H:%M:%S', time.localtime(time.time()))
        logger.info(f"Backup start time is {start_time} end time is {end_time}, pid:{self._job_id}.")
        delete_file(backup_database_sql)
        delete_file(backup_database_rst)
        logger.info(f"Backup database-{self.db_instance} success, pid:{self._job_id}.")
        self.backup_last_log_scn_info()
        return ScriptExitCode.SUCCESS

    def backup_last_log_scn_info(self):
        last_log_scn, last_log_time = get_last_log_scn_info(self.login_params_with_key, self.log_is_backed_up,
                                                            self._job_id)
        last_log_timestamp = add_unix_timestamp(last_log_time)

        self.result_file = f"{self.tmp_path}/result_tmp{self._job_id}"
        self.result_content = f"last_backup_scn;{last_log_scn}\n" \
                              f"timeStamp;{last_log_timestamp}\n" \
                              f"resetlogs_id;{self.resetlogs_id}\n"

        scn_dbf_max_content = f"{last_log_scn} {last_log_time} {last_log_timestamp}\n"
        write_tmp_file(os.path.join(self.additional, "scn_dbf_max"), scn_dbf_max_content)
        logger.info(f"Databackuprst;{last_log_scn};{last_log_time};{last_log_timestamp};{self.resetlogs_id}.")

    def create_pfile_from_spfile(self):
        if self.is_windows_os:
            return create_pfile_from_spfile(self.login_db_params, self.main_backup_path, self.db_name)
        else:
            if not create_pfile_from_spfile_linux(self._pid, self.db_instance, self.oracle_install_user,
                                                  self.main_backup_path,
                                                  self.db_name):
                return ScriptExitCode.ERROR_ORACLE_EXESQL_FAILED
            else:
                return ScriptExitCode.SUCCESS

    def backup_control_file(self):
        self.result_content += f"pfile;tempfile_{self._job_id}\n" \
                               f"BackupLevel;{self.level}\n"
        logger.info(f"pfile: tempfile_{self._job_id}, BackupLevel: {self.level}, pid:{self._job_id}.")

        logger.info(f"Recording database control files, pid: {self._job_id}.")
        db_files_path = os.path.join(self.additional, "dbfiles")
        if os.path.exists(db_files_path):
            delete_file(db_files_path)
        self.get_dbf_info_content(self.dbf_info)
        delete_file(self.dbf_info)
        tmp_rst = f"{self.tmp_path}/get_online_log_info_rst_{self._job_id}.txt"
        ret_code = get_online_redo_log_info(self._pid, self.login_db_params, tmp_rst)
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Get database online log file info failed, pid: {self._job_id}.")
            delete_file(tmp_rst)
            return RETURN_INVALID

        log_files_path = os.path.join(self.additional, "logfiles")
        if os.path.exists(log_files_path):
            delete_file(log_files_path)
        self.get_log_file_content(tmp_rst)
        delete_file(tmp_rst)
        return ScriptExitCode.SUCCESS

    def get_log_file_content(self, file_path):
        log_file_content = ""
        with open(file_path, 'r') as file:
            for line in file:
                log_file_arr = line.split()
                if len(log_file_arr):
                    log_file_content += f"{log_file_arr[0]};{log_file_arr[1]};\n"

        log_files_path = os.path.join(self.additional, "logfiles")
        write_tmp_file(log_files_path, log_file_content)

    def get_dbf_info_content(self, file_path):
        dbf_content = ""
        with open(file_path, 'r') as file:
            for line in file:
                dbf_arr = line.split()
                if len(dbf_arr):
                    dbf_content += f"{dbf_arr[0]};{dbf_arr[1]};{dbf_arr[2]};{dbf_arr[3]}\n"

        db_files_path = os.path.join(self.additional, "dbfiles")
        write_tmp_file(db_files_path, dbf_content)

    def backup_additional_file(self):
        logger.info(f"Copy some additional info, pid: {self._job_id}.")
        first_backup_path = f"{self.additional}\\first_backup_success" \
            if self.is_windows_os else f"{self.additional}/first_backup_success"
        if self.level == 0 and not os.path.exists(first_backup_path):
            create_empty_file(first_backup_path)
            logger.info(f"Create first backup success file success, pid:{self._job_id}.")
        file_list = f"{self.tmp_path}/filelist{self._job_id}"
        get_dir_file_info(self._job_id, self.main_backup_path, file_list)
        self.result_content += f"filelist;filelist{self._job_id}\n"
        logger.info(f"Filelist: filelist{self._job_id}.")
        write_tmp_file(self.result_file, self.result_content)
        params = {
            'oracle_home': self.oracle_home,
            'db_name': self.db_name,
            'db_instance': self.db_instance,
            'asm_id_name': self.asm_id_name,
            'asm_install_user': self.asm_user,
            'db_install_user': self.oracle_install_user,
            'oracle_group': self.oracle_group
        }
        backup_orapw_file(self._job_id, self.additional, params)
        self.get_skip_pdb_table_spaces()

        # 将日志记录置为无效，以免被其他备份任务使用
        ret_code = change_archive_log_unavailable(self._job_id, self.run_rman_params, 1, self.archive, "data")

        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"Change archive log unavailable failed, pid:{self._job_id}.")
            return ret_code
        return ScriptExitCode.SUCCESS

    def check_datafile_consistency(self, data_path):
        logger.info(f'job id: {self._job_id}, start to check datafile consistency')
        # 数据库中查询的数据文件
        datafile_set = self.get_pdb_data_file_set()
        # addition datafiles中获取数据文件
        additional_path = os.path.join(data_path, "additional", "dbfiles")
        logger.info(f'job id: {self._job_id}, additional_path is {additional_path}')
        addition_datafile_set, _ = read_additional_db_file(additional_path, ';')
        logger.info(f'datafile_set: {datafile_set}, addition_datafile_set: {addition_datafile_set}')
        if datafile_set != addition_datafile_set:
            logger.error(f'datafile is not same')
            return False
        # 判断数据文件在文件系统中是否存在
        return exist_addition_file(self._job_id, data_path, addition_datafile_set)

    def get_skip_pdb_table_spaces(self):
        all_pdbs = get_all_pdbs(self._pid, self.db_instance, self.oracle_install_user)
        logger.info(f"get_other_pdb_table_spaces {all_pdbs}")
        pdb_table_space = {}
        for pdb in all_pdbs:
            pdb_name = pdb.get("NAME")
            if pdb_name in self.pdb_names:
                continue
            table_spaces = get_all_table_space_from_pdb(self._pid, self.db_instance, self.oracle_install_user, pdb_name)
            logger.info(f"pdb {pdb_name}, table_spaces: {table_spaces}")
            pdb_table_space[f"{pdb_name}"] = table_spaces
        pdb_table_space_content = ""
        for pdb_name, table_spaces in pdb_table_space.items():
            for table_space in table_spaces:
                pdb_table_space_content += f"{pdb_name}:{table_space}\n"
        pdb_table_space_path = os.path.join(self.additional, "skip_pdb_tablespace")
        if os.path.exists(pdb_table_space_path):
            delete_file(pdb_table_space_path)
        write_tmp_file(pdb_table_space_path, pdb_table_space_content)

    def get_pdb_data_file_set(self):
        login_db_params = {
            'instance_name': self.db_instance,
            'db_install_user': self.oracle_install_user,
            'db_user': get_env_variable(f"job_protectObject_auth_authKey_{self._pid}"),
            'db_password': get_env_variable(f"job_protectObject_auth_authPwd_{self._pid}")
        }
        datafile_rst = f"{self.tmp_path}/get_pdb_data_file_set_rst{self._pid}.sql"
        ret_code = get_pdb_data_files(self._pid, login_db_params, self.pdb_names, datafile_rst)
        if ret_code != ScriptExitCode.SUCCESS:
            logger.error(f"get_pdb_data_files failed, pid:{self._pid}.")
            delete_file(datafile_rst)
            return set()
        datafile_set, _ = read_additional_db_file(datafile_rst, '\s+')
        delete_file(datafile_rst)
        return datafile_set
